using System;
using System.IO;
using System.Collections;
using System.Collections.Generic;
using System.Diagnostics;
using System.Net;
using System.Net.Sockets;
using System.Threading;

namespace LumiSoft.Net
{
	/// <summary>
	/// This is base class for Socket and Session based servers.
	/// </summary>
	public abstract class SocketServer : System.ComponentModel.Component 
	{
        private List<SocketServerSession> m_pSessions           = null;
        private Queue<QueuedConnection>   m_pQueuedConnections  = null;
		private bool                      m_Running             = false;
		private System.Timers.Timer       m_pTimer              = null;
        private BindInfo[]                m_pBindInfo           = null;
		private string                    m_HostName            = "";
		private int                       m_SessionIdleTimeOut  = 30000;
		private int                       m_MaxConnections      = 1000;
		private int                       m_MaxBadCommands      = 8;       
		private bool                      m_LogCmds             = false;

		#region Events declarations

		/// <summary>
		/// Occurs when server or session has system error(unhandled error).
		/// </summary>
		public event ErrorEventHandler SysError = null;

		#endregion

        #region Internal Structs and Classes

        #region struct QueuedConnection

        /// <summary>
        /// This struct holds queued connection info.
        /// </summary>
        private struct QueuedConnection
        {
            private Socket   m_pSocket;
            private BindInfo m_pBindInfo;

            /// <summary>
            /// Default constructor.
            /// </summary>
            /// <param name="socket">Socket.</param>
            /// <param name="bindInfo">Bind info.</param>
            public QueuedConnection(Socket socket,BindInfo bindInfo)
            {
                m_pSocket   = socket;
                m_pBindInfo = bindInfo;
            }

            #region Properties Implementation

            /// <summary>
            /// Gets socket.
            /// </summary>
            public Socket Socket
            {
                get{ return m_pSocket; }
            }

            /// <summary>
            /// Gets bind info.
            /// </summary>
            public BindInfo BindInfo
            {
                get{ return m_pBindInfo; }
            }

            #endregion
        }

        #endregion

        #endregion

        /// <summary>
		/// Default constructor.
		/// </summary>
		public SocketServer()
		{
			m_pSessions          = new List<SocketServerSession>();
			m_pQueuedConnections = new Queue<QueuedConnection>();
			m_pTimer             = new System.Timers.Timer(15000);
			m_pBindInfo          = new BindInfo[]{new BindInfo(IPAddress.Any,10000,false,null)};
			m_HostName           = System.Net.Dns.GetHostName();

			m_pTimer.AutoReset = true;
			m_pTimer.Elapsed += new System.Timers.ElapsedEventHandler(this.m_pTimer_Elapsed);
		}

		#region method Dispose

		/// <summary>
		/// Clean up any resources being used and stops server.
		/// </summary>
		public new void Dispose()
		{
			base.Dispose();

			StopServer();				
		}

		#endregion


		#region method StartServer

		/// <summary>
		/// Starts server.
		/// </summary>
		public void StartServer()
		{
			if(!m_Running){
				m_Running = true;

				// Start accepting ang queueing connections
				Thread tr = new Thread(new ThreadStart(this.StartProcCons));
				tr.Start();

				// Start proccessing queued connections
				Thread trSessionCreator = new Thread(new ThreadStart(this.StartProcQueuedCons));
				trSessionCreator.Start();

				m_pTimer.Enabled = true;
			}
		}

		#endregion

		#region method StopServer

		/// <summary>
		/// Stops server. NOTE: Active sessions aren't cancled.
		/// </summary>
		public void StopServer()
		{
			if(m_Running){
                m_Running = false;

                // Stop accepting new connections
                foreach(BindInfo bindInfo in m_pBindInfo){
                    if(bindInfo.Tag != null){
                        ((Socket)bindInfo.Tag).Close();
                        bindInfo.Tag = null;
                    }
                }

                // Wait method StartProcCons to exit
                Thread.Sleep(100);
			}
		}

		#endregion


		#region method StartProcCons

		/// <summary>
		/// Starts proccessiong incoming connections (Accepts and queues connections).
		/// </summary>
		private void StartProcCons()
		{	
			try{
                CircleCollection<BindInfo> binds = new CircleCollection<BindInfo>();
                foreach(BindInfo bindInfo in m_pBindInfo){
                    Socket s = new Socket(AddressFamily.InterNetwork,SocketType.Stream,ProtocolType.Tcp);
				    s.Bind(new IPEndPoint(bindInfo.IP,bindInfo.Port));
				    s.Listen(500);
                
                    bindInfo.Tag = s;
                    binds.Add(bindInfo);
                }

                // Accept connections and queue them			
				while(m_Running){
					// We have reached maximum connection limit
					if(m_pSessions.Count > m_MaxConnections){
						// Wait while some active connectins are closed
						while(m_pSessions.Count > m_MaxConnections){
							Thread.Sleep(100);
						}
					}

                    // Get incomong connection
                    BindInfo bindInfo = binds.Next();

                    // There is waiting connection
                    if(m_Running && ((Socket)bindInfo.Tag).Poll(0,SelectMode.SelectRead)){
                        // Accept incoming connection
					    Socket s = ((Socket)bindInfo.Tag).Accept();
                                                
        				// Add session to queue
		        		lock(m_pQueuedConnections){
				        	m_pQueuedConnections.Enqueue(new QueuedConnection(s,bindInfo));
					    }
                    }
					
                    Thread.Sleep(2);
				}
			}
			catch(SocketException x){
				// Socket listening stopped, happens when StopServer is called.
				// We need just skip this error.
				if(x.ErrorCode == 10004){			
				}
				else{
					OnSysError("WE MUST NEVER REACH HERE !!! StartProcCons:",x);
				}
			}
			catch(Exception x){
				OnSysError("WE MUST NEVER REACH HERE !!! StartProcCons:",x);
			}
		}

		#endregion
		
		#region method StartProcQueuedCons

		/// <summary>
		/// Starts queueed connections proccessing (Creates and starts session foreach queued connection).
		/// </summary>
		private void StartProcQueuedCons()
		{
			try{
				while(m_Running){
                    // There are queued connections, start sessions.
                    if(m_pQueuedConnections.Count > 0){
                        QueuedConnection connection;
                        lock(m_pQueuedConnections){
                            connection = m_pQueuedConnections.Dequeue();
                        }

                        try{
                            InitNewSession(connection.Socket,connection.BindInfo);
                        }
				        catch(Exception x){
					        OnSysError("StartProcQueuedCons InitNewSession():",x);
					    }
                    }
					// There are no connections to proccess, delay proccessing. We need to it 
					// because if there are no connections to proccess, while loop takes too much CPU.
					else{
						Thread.Sleep(10);
					}
				}
			}
			catch(Exception x){
				OnSysError("WE MUST NEVER REACH HERE !!! StartProcQueuedCons:",x);
			}
		}

		#endregion


		#region method AddSession

		/// <summary>
		/// Adds specified session to sessions collection.
		/// </summary>
		/// <param name="session">Session to add.</param>
		internal protected void AddSession(SocketServerSession session)
		{
			lock(m_pSessions){
				m_pSessions.Add(session);
			}			
		}

		#endregion

		#region method RemoveSession

		/// <summary>
		/// Removes specified session from sessions collection.
		/// </summary>
		/// <param name="session">Session to remove.</param>
		internal protected void RemoveSession(SocketServerSession session)
		{
			lock(m_pSessions){
				m_pSessions.Remove(session);
			}			
		}

		#endregion


		#region method OnSysError

		/// <summary>
		/// 
		/// </summary>
		/// <param name="text"></param>
		/// <param name="x"></param>
		internal protected void OnSysError(string text,Exception x)
		{
			if(this.SysError != null){
				this.SysError(this,new Error_EventArgs(x,new StackTrace()));
			}
		}

		#endregion

		#region method OnSessionTimeoutTimer

		/// <summary>
		/// This method must get timedout sessions and end them.
		/// </summary>
		private void OnSessionTimeoutTimer()
		{
			try{
				// Close/Remove timed out sessions
				lock(m_pSessions){
					SocketServerSession[] sessions = this.Sessions;

					// Loop sessions and and call OnSessionTimeout() for timed out sessions.
					for(int i=0;i<sessions.Length;i++){	                
						// If session throws exception, handle it here or next sessions timouts are not handled.
						try{
							// Session timed out
							if(DateTime.Now > sessions[i].SessionLastDataTime.AddMilliseconds(this.SessionIdleTimeOut)){                                
								sessions[i].OnSessionTimeout();
							}
						}
						catch(Exception x){
							OnSysError("OnTimer:",x);
						}
					}
				}
			}
			catch(Exception x){
				OnSysError("WE MUST NEVER REACH HERE !!! OnTimer:",x);
			}
		}

		#endregion

		#region method m_pTimer_Elapsed

		private void m_pTimer_Elapsed(object sender,System.Timers.ElapsedEventArgs e)
		{	
			OnSessionTimeoutTimer();
		}

		#endregion


		#region virtual method InitNewSession

		/// <summary>
		/// Initialize and start new session here. Session isn't added to session list automatically, 
		/// session must add itself to server session list by calling AddSession().
		/// </summary>
		/// <param name="socket">Connected client socket.</param>
        /// <param name="bindInfo">BindInfo what accepted socket.</param>
		protected virtual void InitNewSession(Socket socket,BindInfo bindInfo)
		{
		}

		#endregion


		#region Properties Implementation

        /// <summary>
        /// Gets or set socket binding info. Use this property to specify on which IP,port server 
        /// listnes and also if is SSL or STARTTLS support.
        /// </summary>
        public BindInfo[] BindInfo
        {
            get{ return m_pBindInfo; }

            set{
                if(value == null){
                    throw new NullReferenceException("BindInfo can't be null !");
                }

                //--- See if bindinfo has changed -----------
                bool changed = false;
                if(m_pBindInfo.Length != value.Length){
                    changed = true;
                }
                else{
                    for(int i=0;i<m_pBindInfo.Length;i++){
                        if(!m_pBindInfo[i].Equals(value[i])){
                            changed = true;
                            break;
                        }
                    }
                }
                //-------------------------------------------

                if(changed){
                    // If server is currently running, stop it before applying bind info.
                    bool running = m_Running;
                    if(running){
                        StopServer();
                    }
     
                    m_pBindInfo = value; 

                    // We need to restart server to take effect IP or Port change
				    if(running){				    
					    StartServer();
				    }
                }
            }
        }

		/// <summary>
		/// Gets or sets maximum allowed connections.
		/// </summary>
		public int MaxConnections 
		{
			get{ return m_MaxConnections; }

			set{ m_MaxConnections = value; }
		}
        
		/// <summary>
		/// Runs and stops server.
		/// </summary>
		public bool Enabled 
		{
			get{ return m_Running; }

			set{				
				if(value != m_Running & !this.DesignMode){
					if(value){
						StartServer();
					}
					else{
						StopServer();
					}
				}
			}
		}
	
		/// <summary>
		/// Gets or sets if to log commands.
		/// </summary>
		public bool LogCommands
		{
			get{ return m_LogCmds; }

			set{ m_LogCmds = value; }
		}

		/// <summary>
		/// Session idle timeout in milliseconds.
		/// </summary>
		public int SessionIdleTimeOut 
		{
			get{ return m_SessionIdleTimeOut; }

			set{ m_SessionIdleTimeOut = value; }
		}
				
		/// <summary>
		/// Gets or sets maximum bad commands allowed to session.
		/// </summary>
		public int MaxBadCommands
		{
			get{ return m_MaxBadCommands; }

			set{ m_MaxBadCommands = value; }
		}

		/// <summary>
		/// Gets or set host name that is reported to clients.
		/// </summary>
		public string HostName
		{
			get{ return m_HostName; }

			set{
				if(value.Length > 0){
					m_HostName = value;
				}
			}
		}

		/// <summary>
		/// Gets active sessions.
		/// </summary>
		public SocketServerSession[] Sessions
		{
			get{ return m_pSessions.ToArray(); }
		}

		#endregion

	}
}
